# CS 4830 - Systems Simulation
# Authors:
#       - SERVER TEAM A
#	- Derek Boone
#	- Brannon Elmore
#       - Andrew Profeta
#       - Ronak Patel
#       - Aaron Pohl
#       - Jesse Williams
#       - Aaron Shelley
#       - Michael Davic
#       - Chris Pickard

""" Switch Example """ 
from SimPy.Simulation import * 
from random import expovariate, seed, random, gauss, randint

## @package SwitchExample
# This package partially satisfies the objectives of Server Sim
# Project

# Maximum number of messages processed per simulated second
# Req. 0803
maxAgeOfDeliverableMessageMicroSeconds = (1000.0)
maxNumMessagesPerMicroSecond = 2000.0
delayPerProcessedMessage = 1.0 / maxNumMessagesPerMicroSecond

## Switch
# Each instance of Switch receives messages sent my other simulated
# components and deliveries messages to other simulated components.
# The switch uses destination address information in each message
# to determine the receiver.
class Switch(Process):
   ## start
   #  Start processing messages and run until simulation exit.
	def start(self, server):
		""" """
		self.numberOfMessagesWaiting = Level(name = 'numWaiting')
		self.statusQueueSize = Level(name = 'statusWaiting')
		self.waitQueue = []
		self.statusQueue = []
		#print "Switch.start()"
      
		while True:
			# get the next available message to deliver
			yield get, self, self.numberOfMessagesWaiting, 1         
			currentMessage = self.waitQueue.pop()
			 
			if currentMessage.flag == 1:		# neither
				print "Received Process Message"
			elif currentMessage.flag == 2:  	# read
				#print "Received Read Message"
				self.statusQueue.append(currentMessage)
				yield put, self, server.switch.statusQueueSize, 1
			else:								# write
				#print "Received Write Message"
				self.statusQueue.append(currentMessage)
				yield put, self, server.switch.statusQueueSize, 1
				
			# Only deliver messages that are not too stale Req. 0803
			while ((now() - currentMessage.originationTime) > maxAgeOfDeliverableMessageMicroSeconds):
				# get the next available message to deliver
				server.numLostMonitor.observe(now() - currentMessage.originationTime)
				yield get, self, self.numberOfMessagesWaiting, 1         
				currentMessage = self.waitQueue.pop()
			
			# We have a message to deliver
			# Wait the required processing rate delay Req. 0803
			yield hold, self, delayPerProcessedMessage

			#deliver message
			server.numDeliveredMonitor.observe(now() - currentMessage.originationTime)

class ProcessorGroup:	
	def __init__(self, isPrimeGroup):
		self.processor0 = Processor0();
		self.processor1 = Processor1();
		self.processor2 = Processor2();
		#self.processor3 = Processor3();
		
class Processor0(Process):
	def start(self, isPrimeGroup):
		#print "Started Processor0"
		self.taskRunTime = 4 # Req: 0601 and 0602
		self.cycleTime = 20  # Req: 0601 and 0602
		if isPrimeGroup:
			self.taskCount = 2
		else:
			self.taskCount = 0
		
		while True:
			#print now(), " Holding Processor0 for 8 ms"
			yield hold, self, self.taskRunTime * self.taskCount
			#print now(), " Holding Processor0 for 12 ms"
			yield hold, self, self.cycleTime - self.taskRunTime * self.taskCount
			#print now(), " Exiting Processor0"
		
# Processor1 in each processor group must run a task called TaskS for a random 
#	amount of time between 50 microseconds and 4 milliseconds every time 
#	a message is sent via the switch on behalf of the same processor group
class Processor1(Process):
	def start(self):
		print "Started Processor1"

class Processor2(Process):
	def start(self, isPrimeGroup):
		#print "Started Processor2"
		self.taskRunTime = 4 # Req: 0603 and 0604
		self.cycleTime = 20  # Req: 0603 and 0604
		if isPrimeGroup:
			self.taskCount = 2
		else:
			self.taskCount = 0
		
		while True:
			#print now(), " Holding Processor2 for 8 ms"
			yield hold, self, self.taskRunTime * self.taskCount
			#print now(), " Holding Processor2 for 12 ms"
			yield hold, self, self.cycleTime - self.taskRunTime * self.taskCount
			#print now(), " Exiting Processor2"

#class Processor3(Process):
#	def start:
## Message
class Message:
   """ """
   ## __init__
   #
   #  This method initializes 
   def __init__(self, destination, originationTime, size, flag):
    # switch
	self.destination = destination
	self.originationTime = originationTime
	self.size = size
	self.flag = flag

## Server
#  Each Server ...
class Server:
	""" """
    ## __init__
    #
    #  This method initializes the instance's elevator and list of floors
	def __init__(self):
		# switch 
		self.switch = Switch()
		self.processorGroup = ProcessorGroup(True)
		self.numDeliveredMonitor = Monitor(name = 'numDelivered')
		self.numLostMonitor = Monitor(name = 'numLost')
		self.diskController = DiskController()


## MessageGenerator
class MessageGenerator(Process):
	""" """
	## start
	#
	def start(self, server):
		i = 0
		while True:
			#print "MessageGenerator.start()"
			delay = expovariate(1.0/2.5) # Req. ????
			flagtype = randint(1,3) 
			yield hold, self, delay
			if flagtype == 1: # neither
				newMessage = Message(destination = " ", originationTime = now(), size = 1, flag = 1)
			elif flagtype == 2: # read
				newMessage = Message(destination = " ", originationTime = now(), size = 1, flag = 2)
			else: # write 
				newMessage = Message(destination = " ", originationTime = now(), size = 1, flag = 3)
			server.switch.waitQueue.append(newMessage)
			yield put, self, server.switch.numberOfMessagesWaiting, 1
			
			i = i + 1

		 
# Req: 0901  Every status message received by Disk Controller shall write 600 bytes to Disk.
# Req: 0902  Disk Controller shall be capable of writing to Disk at a maximum rate of 30 Mbytes per second.
# Req: 0903  Disk Controller shall be capable of reading from Disk at a maximum rate of 60 Mbytes per second.
# Req: 0904  Disk Controller shall require 10000 us every time there is a transition from reading to writing or vice versa e.g. back to back reads are not penalized but the sequence read, write, read is penalized 10000 us for each transition.
class DiskController(Process):
	def start(self, server):
		self.state = -1								# Assuming initial state is neither Read or Write
		while True:
			print "Starting DiskController"
			yield get, self, server.switch.statusQueueSize, 1
			currentMessage = server.switch.statusQueue.pop()
			if currentMessage.flag == 2:
				print "Disk Controller received read message"
				if self.state == 2:
					# Assuming we read 600 bytes
					yield hold, self, 0.009535 		# (600 / (60 * 1024 * 1024)) * 1000 
				else: 
					print "Transition"
					yield hold, self, 10			# Req 0904: 10 milliseconds to change state
					yield hold, self, 0.009535 		# (600 / (60 * 1024 * 1024)) * 1000
					self.state = 2
				
			elif currentMessage.flag == 3:
				print "Disk Controller received write message"
				if self.state == 3:
					yield hold, self, 0.01907 		# (600 / (30 * 1024 * 1024)) * 1000 
				else: 
					print "Transition"
					yield hold, self, 10			# Req 0904: 10 milliseconds to change state
					yield hold, self, 0.01907 		# (600 / (30 * 1024 * 1024)) * 1000
					self.state = 3
			print "Exiting DiskController"
	
	
## Simulation
#  The Simulation starts, runs for a predetermined period of
# simulated time, displays cumulative statistics, and then exist.
# Req. 0001 The simulation executes in only one mode
# Req. 0101 The simulations shall present data as a result of execution that
#           allows the user to determine any bottlenecks present in the simulation
theseeds = [1, ] # random number generator seeds for each trial

## Experiment/Result printer
#  Initialize the simulation, create a simulated server,
# create a simulated message generator, and execute the simulation
# for a predetermined amount of simulated time.
for currentSeed in theseeds:
   seed(currentSeed)

   initialize()
   server = Server()
   activate(server.switch, server.switch.start(server))
   generator = MessageGenerator("MessageGenerator")
   activate(generator, generator.start(server))
   
   # Activate Processors
   activate(server.processorGroup.processor0, server.processorGroup.processor0.start(True))
   #activate(server.processorGroup.processor1, server.processorGroup.processor1.start())
   activate(server.processorGroup.processor2, server.processorGroup.processor2.start(True))
   activate(server.diskController, server.diskController.start(server))
   simulate(until = 120) # Req. ????

   print "Number delivered: %d"%(server.numDeliveredMonitor.count())
   print "Average delivery delay: %7.4fus"%(server.numDeliveredMonitor.mean())
   print "Number lost: %d"%(server.numLostMonitor.count())
   
   # The following lines will fail if server.numLostMonitor.count == 0
   #print "Average time in queue for lost: %7.4fus"%(server.numLostMonitor.mean())
   #print "Maximum time in queue for lost: %7.4fus"%(max(server.numLostMonitor.yseries()))
